package main;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import clasificador.ClasificadorEntrenado;
import clasificador.ValidadorCruzado;
import clasificador.naivebayes.ClasificadorNaiveBayes;
import datos.Atributo;
import datos.AtributoFinito;
import datos.CSVBuilder;
import datos.Dato;
import datos.Muestra;
import datos.MuestraClasificada;

public class MainBayes {
    private static PrintStream out = System.out;

    private static int test (String filename) {
    	BufferedReader f = null;
        try {
        	f = new BufferedReader(new FileReader(filename));
            List<Atributo<Object>> header = MainBayes.parseCSVHeader(f);
            Atributo<Object> clase = header.get( header.size() - 1 );

            List<Atributo<?>> header2 = new Vector<Atributo<?>>(header);
            
            CSVBuilder<Dato, Muestra> builder = 
                new CSVBuilder<Dato, Muestra>(
                        new Dato.Builder(header),
                        new Muestra.Builder(header2)
                        );

            String line = f.readLine();
            while (line != null) {
                builder.add(line);
                line = f.readLine();
            }

            MuestraClasificada<Object> muestra = new MuestraClasificada<Object>(builder.finish(), (Atributo<Object>)clase); 

            ClasificadorEntrenado<Object> clasificador = new ClasificadorNaiveBayes(muestra);

            for (Dato entry : muestra.getDatos()) {
                out.print("[");
                for (Atributo<Object> attr : header) {
                    out.print(entry.getAtributo(attr) + " ");
                }
                out.println("]");
                Object c = clasificador.clasificar(entry);
                out.println("-> " + clase);
            }
            
            f.close();

        } catch (Exception ex) {
            ex.printStackTrace();
            try
            {
            	if(f != null) f.close();
            } catch(Throwable e) {}
        }
        
        

        return 0;
    }

    public static void main(String [] args) {
        if (args.length < 1) {
            MainBayes.printUsage();
            MainBayes.exit(1);
        }

        if (args[0].equals("TEST")) {
            MainBayes.exit(MainBayes.test(args[1]));
        }
        String filename = args[0];
        
        Map<String,Object> parametrosClasificacion = new HashMap<String,Object>();
        
        if(args.length > 1)
        {
            parametrosClasificacion.put("parametroLaplace",new Integer(Integer.parseInt(args[1])));
            
            if(args.length > 2)
            {
                parametrosClasificacion.put("ignorarDegenerados", Boolean.parseBoolean(args[2]));
            }
        }        
        
        int returnCode = 0;
        BufferedReader file = null;
        try {
        	file = new BufferedReader(new FileReader(filename));
            MuestraClasificada<Object> muestra;

            List<Atributo<Object>> header = MainBayes.parseCSVHeader(file);
            out.println("Validando sobre la muestra " + filename);
            out.println("Atributos: ");
            for (Atributo<Object> atributo : header) {
                out.println("\t" + atributo.getNombre() + " : " + atributo.getTipo());
            }

            /* Esto es feo */
            Atributo<Object> clase = header.get(header.size() - 1);

            List<Atributo<?>> header2 = new Vector<Atributo<?>>(header);
            
            CSVBuilder<Dato, Muestra> builder = 
                new CSVBuilder<Dato, Muestra>(
                        new Dato.Builder(header),
                        new Muestra.Builder(header2)
                        );

            String line = file.readLine();
            while (line != null) {
                builder.add(line);
                line = file.readLine();
            }

            muestra = new MuestraClasificada<Object>(builder.finish(), (Atributo<Object>)clase); 
            out.println("Leidas " + muestra.getDatos().size() + " entradas");

            Class<ClasificadorNaiveBayes> clasificador = 
                (Class<ClasificadorNaiveBayes>)(ClasificadorNaiveBayes.class);
            ValidadorCruzado v = new ValidadorCruzado(clasificador, 10, 1);
            ValidadorCruzado.ResultadosValidacionCruzada resultados = v.validar(muestra,parametrosClasificacion);
            printResults(filename, resultados);
            file.close();
        } catch (Exception ex) {
            out.println("Error procesando el fichero " + filename);
            /* Aquí no usamos printStackTrace porque se carga el orden de las impresiones */
            out.println("\t" + ex);
            for (StackTraceElement stelt : ex.getStackTrace()) {
                out.println("\t\t" + stelt);
            }
            returnCode = 1;
            ex.printStackTrace();            
                        
            try
            {
            	if(file != null)
            	{
            		file.close();
            	}
            } catch(Throwable e) {}
        }
        MainBayes.exit(returnCode);
    }

    private static List<Atributo<Object>> parseCSVHeader(BufferedReader file) throws IOException {
        List<Atributo<Object>> header = new ArrayList<Atributo<Object>>();
        String line = file.readLine();
        while (!line.equals("")) {
            header.add(MainBayes.parseAttribute(line));
            line = file.readLine();
        }
        return header;
    }

    private static AtributoFinito<Object> parseFinite(String name, String type, String values) {
        if (values == null) {
            throw new RuntimeException("Malformed CSV header: no range for finite attribute " + name);
        }

        Set<String> valueSet = new HashSet<String>();
        for (String str : values.split("\\|")) {
            valueSet.add(str.trim());
        }

        if(type.equals("string"))
        {
            return (AtributoFinito)new AtributoFinito<String>(String.class, name, valueSet) {
                public String readDato(String s) { return s.trim(); }
            };
        } else if(type.equals("int")) {
            Set<Integer> intSet = new HashSet<Integer>();
            for (String s : valueSet) {
                intSet.add(Integer.parseInt(s));
            }
            return (AtributoFinito)new AtributoFinito<Integer>(Integer.class, name, intSet) {
                public Integer readDato(String s) { return Integer.parseInt(s); }
            };
        } else {
            throw new RuntimeException("Error parsing CSV header: undefined type " + type);
        }
    }

    private static Atributo<Object> parseInfiniteDiscrete(String name, String type) {        
       if(type.equals("string"))
       {
           return (Atributo)new Atributo<String>(String.class, name) {
                public String readDato(String s) { return s.trim(); }
            };
       } else if(type.equals("int")) {
            return (Atributo)new Atributo<Integer>(Integer.class, name) {
                public Integer readDato(String s) { return Integer.parseInt(s); }
            };
       } else {
    	   throw new RuntimeException("Error parsing CSV header: undefined type " + type);
        }
    }
    private static Atributo<Object> parseAttribute(String s) {
        String [] tokens = s.split(":");
        if (tokens.length != 2 && tokens.length != 3) {
            throw new RuntimeException("Malformed or empty CSV header");
        }

        String name = tokens[0].trim();
        String type = tokens[1].trim();
        
        if(type.equals("string") || type.equals("int"))
        {
            if (tokens.length == 3) {
                return parseFinite(name, type, tokens[2]);
            } else {
                return parseInfiniteDiscrete(name,type);
            }
        } else if(type.equals("double")) {
            return (Atributo)new Atributo<Double> (Double.class, name) {
                public Double readDato(String s) { return Double.parseDouble(s); }
            };
        } else {
        	throw new RuntimeException("Cabecera del fichero CSV inválida");
        }
    }

    private static String executableName() {
        return new java.io.File(MainBayes.class.getProtectionDomain()
                .getCodeSource()
                .getLocation()
                .getPath())
            .getName();
    }
    private static void printResults(String filename,
            ValidadorCruzado.ResultadosValidacionCruzada resultados) {

        out.println(
                "Resultados para la muestra " + filename
                );
        int particion = 1;
        Double suma = 0.;
        for (Double error : resultados.getErrores()) {
            suma += error;
            out.println("\tParticion de test " + particion);
            out.println("\t\tProporcion de errores: " + error);
            out.println("\t\tProporcion de aciertos: " + (1.0 - error));
            particion++;
        }
        out.println("Error medio: " + (suma / resultados.getErrores().size()));
    }
    private static void printUsage() {
        out.println("Uso:");
        out.println("\tjava -jar "
                + MainBayes.executableName() + " <muestra> [<parametro-laplace> [<ignorar-degenerados>]]"
                );
    }
    private static void exit(int k) {
        System.exit(k);
    }
}
